Skip to content

refactor(routing): introduce authoritative provider routing plan - #229

Merged
GionaGranchelli merged 6 commits into
masterfrom
refactor/0.6.0-provider-routing-plan
Aug 14, 2026
Merged

refactor(routing): introduce authoritative provider routing plan#229
GionaGranchelli merged 6 commits into
masterfrom
refactor/0.6.0-provider-routing-plan

Conversation

@GionaGranchelli

@GionaGranchelli GionaGranchelli commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Implements Epic 2.2: there is now exactly one immutable representation of configured provider routingProviderRoutingPlan in tramai-core. Every execution, validation, framework-composition, sovereign-restriction, and routing-evidence path works from that plan rather than maintaining its own copy.

Change class: runtime-behaviour. Intentional behaviour changes: duplicate providers and structurally invalid routing configurations that were previously accepted now fail fast at construction with ConfigurationException.

What

tramai-core

  • ProviderRoutingPlan (new): immutable snapshot of providers, routes, defaultProvider with typed @JvmInline ProviderId/ModelId value classes. Builder enforces fail-fast validation: blank/whitespace IDs (providers and models), duplicate provider IDs (no more silent replacement), unknown primary/fallback providers, duplicate primaries, duplicate identical fallback routes, fallback-identical-to-primary, fallback-without-primary, unknown default provider, degenerate route structures. Defensive copies wrapped in Collections.unmodifiableMap/unmodifiableList — mutation after build throws.
  • Route role is explicit: primaries and fallbacks are tracked separately in the builder and composed at build(), so fallback-before-primary registration order is preserved (the old index-0 inference could drop a fallback registered before its primary).
  • ProviderRegistry (rewritten): compatibility façade over the plan. Public API unchanged (builder(), singleProvider, provider, model, fallbackModel, fallbackProvider, defaultProvider, resolve, resolveCandidates). No duplicate backing maps — state exists only in the plan. ProviderRoute/ResolvedProviderRoute JVM shapes unchanged.
  • fallbackProvider(model, provider) deliberately keeps the same effective model on another provider — not classified as a self-loop. No recursive fallback routing introduced.

tramai-engine

  • EngineComponents.ProviderComponents now freezes ProviderRoutingPlan (not a second registry representation). TramaiInvocationHandler resolves candidates from the plan. Published TramaiEngine(providerRegistry=...) constructors untouched.

tramai-standalone

  • Tramai.Builder.buildRoutingPlan() freezes the authoritative plan once; build() reuses the same instance (identity, not a reconstructed copy). Post-build builder mutations have no effect on the built runtime. providerRegistry back to private.

tramai-sovereign

  • Deleted shadow routing state: registeredProviders, primaryModelRoutes, fallbackRoutes, defaultProviderName, FallbackRoute.
  • New SovereignRoutingValidationPolicy validates the same frozen plan (allowedProviders, providerZones, allowedModels incl. primary effective models, allowedFallbackProviders, default-provider checks, offline-mode LOCAL constraints). Validation runs before runtime construction — an invalid routing config fails at build without leaving a partially-built instance. Artifact-verification targets derive from the plan. No @Suppress("INVISIBLE_MEMBER") reach-through.

tramai-spring

  • Provider precedence resolved before the plan builder: property-backed providers and ModelProvider beans merge into one unique set (LinkedHashMap, unique bean overrides same-id property provider, deterministic order). Property-vs-property duplicates and genuine duplicate user beans pass through to the plan builder and fail deterministically. No Spring-side route validator.

Docs

  • docs/ROADMAP-0.6.0.md (Epic 2.2 ✅ Complete), CHANGELOG.md, docs/modules/tramai-spring.md, docs/modules/tramai-sovereign.md.

Fix Round 1 (review findings addressed)

Finding Fix
P1: plan collections mutable after build Collections.unmodifiableMap/unmodifiableList; regression test asserts UnsupportedOperationException on as MutableMap/as MutableList mutation
P1: fallback-only route masquerades as sovereign primary (unapproved effective model) Builder tracks explicit primaries; fallback-without-primary rejected at build; sovereign validates primary effective models; repro test added
P2: duplicate primary routes silently replace Rejected at build (Duplicate primary route for model 'X')
P2: fallback identical to primary accepted Rejected at build (duplicates its primary route)
P2: Spring property-vs-property duplicates collapsed Split out and passed to plan builder → deterministic Duplicate provider 'openai'; test added
P2: model whitespace accepted validateModelId mirrors provider validation (value == value.trim())
P2: sovereign reached standalone internals via @Suppress Tramai.Builder.buildRoutingPlan() public freeze; sovereign validates same instance before runtime build; suppression removed
P2: synthetic JVM ctor descriptor removed from api dump Accepted as non-contractual — see ABI note below
(extra) fallback registered before primary silently dropped Explicit primary/fallback split fixes order-dependence; test added

ABI note (reviewed, deliberate)

The api dump no longer records the pre-0.6.0 synthetic (Map, Map, String, DefaultConstructorMarker) constructor of ProviderRegistry. That descriptor is a DefaultConstructorMarker marker for a private constructor — DefaultConstructorMarker itself cannot be instantiated by consumers, and the committed binary-compatibility fixture does not exercise ProviderRegistry (verified: the fixture jar only touches BinaryCompatFixtureKt and StructuredOutputBinaryCompatFixture). The public surface — companion factories, builder, resolve/resolveCandidates, and both DTO shapes — is byte-compatible with 0.5.0. The alternative (an ABI bridge ctor) introduced a mutable field flagged as global state by the maintainability scanner; dropping the bridge removes both the scanner findings and the maintenance cost. The api dump diff is otherwise additive-only.

Verification

  • ./gradlew :tramai-core:test :tramai-engine:test :tramai-standalone:test :tramai-sovereign:test :tramai-spring:test --rerun-tasksall green, 0 failures
  • ./gradlew apiCheck (all tramai modules) — PASSED. Note: examples:governed-workflow:apiCheck fails on a pre-existing master drift (buildGovernedNetworkPolicyWorkflow in source, never dumped); this PR touches zero example files
  • ./gradlew verifyPr -PchangeClass=runtime-behaviourPASSED (268 tasks: maintainability baseline, change policy, build-logic tests, all subproject tests)
  • ./gradlew verifyCancellationSafetyPASSED (no new findings)
  • agy independent re-review after fix round 1 — merge-ready, 0 findings

Fix Round 2 (review findings addressed)

Finding Fix
P2: Tramai.Builder permanently cached the first routing plan; routing mutations after the first build()/buildRoutingPlan() were silently ignored by later builds (and sovereign retry validated a stale plan) Every routing mutator (provider, model, fallbackModel, fallbackProvider, defaultProvider) now calls invalidateRoutingPlan(). Already-built runtimes keep their immutable plan; the next build sees the new routing state. Sovereign validates and installs the same frozen instance because no mutation occurs between freeze and build
P3: MQ-0004 rationale named local variables instead of the actual findings Reason rewritten around the builder/validation mutable collections (per-model fallback staging list, duplicate-detection set)
P3: journal referenced volatile head commit 2407b4d Removed; journal now describes the branch without pinning a commit
P3: roadmap claimed "public API unchanged" while adding routing-plan APIs Wording: "existing public API preserved; additive routing-plan APIs introduced" with the additive surface enumerated; SovereignTramai.Builder.provider() KDoc now documents build-time ConfigurationException

Regression tests added: standalone build A → mutate routing → build B (A stays frozen, B sees mutation, call counts 1/1); sovereign validation failure → add missing route → retry succeeds.

Non-claims

  • Does NOT introduce recursive fallback routing or graph-cycle detection (fallback-provider same-model configs are valid, not self-loops).
  • Does NOT change ProviderRoute/ResolvedProviderRoute public JVM shapes.
  • Does NOT add a Spring-side route validator.
  • Does NOT change TramaiProperties schema or property semantics.
  • Does NOT modify the V1 evidence schema.
  • config/quality/0.6.0-baseline.json is byte-identical to origin/master (the immutable v0.5.0 canonical measurement). MQ-0004 deviation allowed raised 3→4 for the two new ProviderRoutingPlan builder mutable collections, resolved in the 0.7.0 registry migration.

This PR needs review before merge.

Introduce ProviderRoutingPlan as the single immutable snapshot of configured
provider routing (providers, routes, defaultProvider) with typed ProviderId/
ModelId value classes and fail-fast build-time validation. Duplicate provider
registration now fails with ConfigurationException instead of silently
replacing the earlier registration; blank IDs, unknown primary/fallback
providers, duplicate fallback routes, and unknown defaults are rejected at
construction.

ProviderRegistry remains a public compatibility facade over the plan with
unchanged API and resolution order (ProviderRoute/ResolvedProviderRoute JVM
shapes unchanged). The engine freezes the plan into EngineComponents.
ProviderComponents; standalone composes through the plan builder;
SovereignTramai.Builder deletes its shadow routing state and validates the
shared plan via SovereignRoutingValidationPolicy; Spring resolves
property-provider vs bean precedence into one unique provider set before the
plan builder (explicit beans still override property-backed providers;
genuine duplicate user beans fail deterministically). Routing-related
sovereign evidence and artifact-verification targets derive from the same
frozen plan. Epic 2.2 complete.
Copilot AI lite review requested due to automatic review settings August 13, 2026 22:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Epic 2.2 by introducing ProviderRoutingPlan (in tramai-core) as the single immutable, authoritative representation of provider routing, and refactors standalone composition, engine execution, sovereign validation, and Spring auto-configuration to derive routing behavior from that frozen plan (with stricter fail-fast validation via ConfigurationException).

Changes:

  • Added ProviderRoutingPlan with typed IDs (ProviderId/ModelId) and build-time validation, and rewrote ProviderRegistry as a compatibility facade over the plan.
  • Updated engine, standalone, sovereign, and Spring wiring to freeze/consume the plan as the single routing source of truth.
  • Added/updated tests and docs to reflect the new validation and routing-plan model.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tramai-standalone/src/test/kotlin/dev/tramai/standalone/TramaiComponentCompositionTest.kt Adds build-time failure assertions for invalid routing; aligns provider IDs and model mapping in composition tests.
tramai-standalone/src/main/kotlin/dev/tramai/standalone/Tramai.kt Switches standalone builder to build a ProviderRoutingPlan, then wraps it via ProviderRegistry.from(...).
tramai-spring/src/test/kotlin/dev/tramai/spring/TramaiAutoConfigurationTest.kt Adds Spring tests for provider override precedence and fail-fast duplicate/invalid fallback routing.
tramai-spring/src/main/kotlin/dev/tramai/spring/TramaiAutoConfiguration.kt Merges property-backed providers and bean providers before registering into the canonical plan builder.
tramai-sovereign/src/test/kotlin/dev/tramai/sovereign/SovereignTramaiTest.kt Updates sovereign tests to expect ConfigurationException for invalid routing configs.
tramai-sovereign/src/test/kotlin/dev/tramai/sovereign/SovereignRoutingValidationPolicyTest.kt New tests ensuring sovereign validation reads providers/routes from the authoritative plan.
tramai-sovereign/src/main/kotlin/dev/tramai/sovereign/SovereignTramai.kt Removes sovereign shadow routing state; validates and derives verification targets from the plan.
tramai-sovereign/src/main/kotlin/dev/tramai/sovereign/SovereignRoutingValidationPolicy.kt Introduces plan-based sovereign routing validation and verificationTargets() extension.
tramai-engine/src/test/kotlin/dev/tramai/engine/EngineComponentsTest.kt Adds assertions that engine freezes and preserves routing plan ordering and explicit provider resolution.
tramai-engine/src/main/kotlin/dev/tramai/engine/TramaiEngine.kt Switches invocation routing from ProviderRegistry to ProviderRoutingPlan.resolveCandidates(...).
tramai-engine/src/main/kotlin/dev/tramai/engine/components/EngineComponents.kt Changes ProviderComponents to carry the frozen ProviderRoutingPlan.
tramai-engine/src/main/kotlin/dev/tramai/engine/components/EngineComponentFactory.kt Freezes the routing plan into ProviderComponents at component construction.
tramai-core/src/test/kotlin/dev/tramai/core/provider/ProviderRoutingPlanTest.kt New unit tests for plan ordering, validation, immutability snapshot behavior, and identity preservation.
tramai-core/src/test/kotlin/dev/tramai/core/provider/ProviderRegistryCompatibilityTest.kt New tests proving legacy ProviderRegistry API preserves routing semantics while backed by the plan.
tramai-core/src/main/kotlin/dev/tramai/core/provider/ProviderRoutingPlan.kt New authoritative routing plan model + resolution helpers.
tramai-core/src/main/kotlin/dev/tramai/core/provider/ProviderRegistry.kt Rewritten as compatibility facade over ProviderRoutingPlan; exposes routingPlan.
tramai-core/api/tramai-core.api API dump updates for new routing plan types and ProviderRegistry.from(...) / routingPlan.
docs/ROADMAP-0.6.0.md Marks Epic 2.2 complete and documents the implemented routing plan model.
docs/modules/tramai-spring.md Updates Spring module flow to describe provider merge/override behavior before plan build.
docs/modules/tramai-sovereign.md Updates sovereign build-time validation description to validate the immutable plan.
CHANGELOG.md Adds changelog entry describing authoritative routing plan and validation behavior changes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +56 to +58
val requestedModelId = ModelId(requestedModelName)
routes[requestedModelId] = routes.getOrPut(requestedModelId) { emptyList() } +
PlannedProviderRoute(ProviderId(providerName), ModelId(fallbackModelName))
Comment on lines +108 to +112
private fun validateModelId(modelId: ModelId) {
if (modelId.value.isBlank()) throw ConfigurationException("Model name must not be blank")
}
}
}
Comment on lines +184 to +188
val providersById = propertyProviders.toMap() + uniqueBeanProviders.associate { it.providerId() to it }
providersById.forEach { (providerId, provider) ->
builder.provider(provider, name = providerId)
}
duplicateBeanProviders.forEach { provider -> builder.provider(provider, name = provider.providerId()) }
Comment on lines +379 to +381
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
val plan = tramai.providerRegistry.routingPlan
SovereignRoutingValidationPolicy.validate(plan, profile)
…ity, ABI bridge

- ProviderRoutingPlan: unmodifiableMap/unmodifiableList defensive copies;
  explicit primary+fallback split (fallback-only rejected, duplicate primary
  rejected, fallback==primary rejected, order-independent fallback registration);
  model whitespace validation mirrors provider validation.
- ProviderRegistry: legacy (Map,Map,String) primary ctor preserves the 0.5.0
  synthetic DefaultConstructorMarker descriptor; plan-backed secondary restores
  the exact frozen plan instance (identity, not a reconstructed copy).
- Tramai.Builder.buildRoutingPlan() freezes the authoritative plan once; build()
  reuses the same instance. providerRegistry back to private.
- SovereignTramai: validates the frozen plan BEFORE constructing the runtime
  (no abandoned instance); @Suppress INVISIBLE_MEMBER reach-through removed.
- SovereignRoutingValidationPolicy: primary effective model must be allowed.
- Spring: property-vs-property duplicates no longer collapse; they reach the
  plan builder and fail deterministically. Bean-over-property precedence intact.
- Tests: immutability mutation, fallback-only, duplicate primary, fallback==primary,
  whitespace, fallback-before-primary, Spring collision, sovereign rejection.
…anonical baseline

The DefaultConstructorMarker descriptor for ProviderRegistry's private
constructor is non-contractual (marker cannot be instantiated by consumers;
binary-compat fixture does not exercise the class). The bridge introduced a
mutable plan field flagged as global state; removing it eliminates the
scanner findings and the maintenance cost.

- ProviderRegistry: single plan-backed private ctor; no legacy bridge.
- config/quality/0.6.0-baseline.json: restored from origin/master (immutable
  v0.5.0 canonical measurement; earlier session had wrongly regenerated it).
- maintainability-deviations.yml MQ-0004: allowed 3->4 for the two new
  ProviderRoutingPlan builder mutable collections (requestedModelId,
  seenFallbacks), resolved in the 0.7.0 registry migration.
…talog, module-dependency-graph) from PR #229

These files were rewritten by the earlier session's generateMaintainabilityBaseline
run, not by the routing work. They drift from the canonical origin/master copies.
…ation

P2: Tramai.Builder cached its first builtRoutingPlan forever; routing
mutations after the first build()/buildRoutingPlan() were silently ignored
by later builds. Every routing mutator now calls invalidateRoutingPlan() so
a reusable builder sees new routing state while already-built runtimes keep
their immutable plan. Sovereign validation-failure retry no longer validates
a stale cached plan.

P3: MQ-0004 rationale now describes the actual builder/validation mutable
collections (not local variables); roadmap API wording changed to
'existing public API preserved; additive routing-plan APIs introduced';
journal dropped the volatile head commit; SovereignTramai.Builder.provider()
KDoc reflects build()-time ConfigurationException validation.

Tests: standalone builder A->mutate->B sees new routing; sovereign
validation failure -> add missing route -> retry succeeds.
@GionaGranchelli
GionaGranchelli merged commit 1a539d7 into master Aug 14, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants